<!DOCTYPE html>
<html>
<head>
    <meta charset="UTF-8">
    <link rel="icon" href="">
    <title>ClickHouse Query</title>

    <!-- Code Style:

    Do not use any JavaScript or CSS frameworks or preprocessors.
    This HTML page should not require any build systems (node.js, npm, gulp, etc.)
    This HTML page should not be minified, instead it should be reasonably minimalistic by itself.
    This HTML page should not load any external resources on load.
    (CSS and JavaScript must be embedded directly to the page. No external fonts or images should be loaded).
    This UI should look as lightweight, clean and fast as possible.
    All UI elements must be aligned in pixel-perfect way.
    There should not be any animations except the progress bar.
    No unexpected changes in positions of elements while the page is loading.
    Navigation by keyboard should work.
    64-bit numbers must display correctly.

    -->

    <!-- Development Roadmap:

    1. Support readonly servers.
    Check if readonly = 1 (with SELECT FROM system.settings) to avoid sending settings. It can be done once on address/credentials change.
    It can be done in background, e.g. wait 100 ms after address/credentials change and do the check.
    Also, it can provide visual indication that credentials are correct.

    Nice to have:
    - display totals
    - show/hide the editor
    - check for readonly
    - query caching
    - downloads in any format
    - history navigation
    - short links by saving in a special table
    - sharing by creating a short-living user
    - notebook mode

    -->

    <style>
        :root {
            --background-color-rotate: 215;
            --background-color: oklch(96% 0.0296 var(--background-color-rotate));
            --element-background-color: #FFF;
            --element-color-ok: #080;
            --element-background-color-fail: #FEE;
            --bar-color: #FF8; /* Light bar in background of table cells. */
            --border-color: #CCC;
            --shadow-color: rgba(0, 0, 0, 0.1);
            --button-color: #FF0;
            --text-color: #000;
            --text-color-active: #000;
            --button-active-color: #FE0;
            --button-active-text-color: #000;
            --misc-text-color: #888;
            --error-color: #FEE; /* Light-pink on light-cyan is so neat, I even want to trigger errors to see this cool combination of colors. */
            --table-header-color: #EEE;
            --table-hover-filter: brightness(0.9);
            --link-color: #06D;
            --logo-color: #FFF;
            --border-color-selected: #F80;
            --menu-toggle-color: #000;

            --progress-color: #0C4;
            --progress: 0%;

            ::selection {
                color: #FFF;
                background-color: #456;
            }
        }

        [data-theme="dark"] {
            --background-color-rotate: 250;
            --background-color: oklch(15% 0.0296 var(--background-color-rotate));
            --element-background-color: #000;
            --element-color-ok: #8F8;
            --element-background-color-fail: #300;
            --bar-color: #320;
            --border-color: #444;
            --shadow-color: rgba(0, 0, 0, 0.1);
            --text-color: #CCC;
            --text-color-active: #FFF;
            --button-color: rgb(250 255 105);
            --button-text-color: #000;
            --button-active-color: #FE0;
            --button-active-text-color: #000;
            --misc-text-color: #888;
            --error-color: #400;
            --table-header-color: #102020;
            --table-hover-filter: contrast(0.9) brightness(2);
            --link-color: #4BDAF7;
            --logo-color: #444;
            --border-color-selected: #FF0;
            --menu-toggle-color: #FF0;

            --progress-color: #FF0;

            ::selection {
                color: #000;
                background-color: #FF8;
            }
        }

        *
        {
            box-sizing: border-box;
            /* For iPad */
            margin: 0;
            border-radius: 0;
            tab-size: 4;
        }

        html, body
        {
            height: 100%;
            margin: 0;
            /* This enables position: sticky on controls */
            overflow: auto;
            scrollbar-color: var(--border-color) var(--element-background-color);
        }

        html
        {
            /* The fonts that have full support for hinting. */
            font-family: Roboto Mono, Liberation Sans, DejaVu Sans, sans-serif, Noto Color Emoji, Apple Color Emoji, Segoe UI Emoji;
            background: var(--background-color);
            color: var(--text-color);
        }

        body
        {
            position: relative;
            display: grid;
            grid-template-columns: auto 1fr;
            grid-template-areas: "menu main";
        }

        #menu
        {
            padding: 0.25rem;
            grid-area: menu;
            position: relative;
            background: var(--table-header-color);
            border-right: 1px solid var(--border-color);
            display: grid;
            grid-template-rows: auto 1fr auto;
            grid-template-areas: "databases-icon" "contents" "github-icon";
        }

        #main
        {
            overflow: auto;
        }

        .menu-icon
        {
            width: 2.25em;
            cursor: pointer;
            user-select: none;
            fill: var(--menu-toggle-color);
        }

        .menu-icon:hover
        {
            transform: translate(1px, 1px);
        }

        #databases-icon
        {
            grid-area: databases-icon;
        }

        #databases
        {
            grid-area: contents;
        }

        #github-icon
        {
            grid-area: github-icon;
        }

        #github-icon-light, #github-icon-dark
        {
            display: none;
        }

        [data-theme="light"] #github-icon-light, [data-theme="dark"] #github-icon-dark
        {
            display: block;
        }

        .database button
        {
            cursor: pointer;
            background: transparent;
            font-size: 100%;
            padding: 0;
            user-select: none;
            color: var(--text-color);
            border-bottom: 1px dashed var(--text-color);
        }

        .database button:hover
        {
            color: var(--button-text-color);
            background-color: var(--button-color);
        }

        .database button:focus
        {
            color: var(--button-text-color);
            background-color: var(--button-color);
        }

        .no-tables
        {
            color: var(--misc-text-color);
        }

        .tables
        {
            padding-left: 1rem;
        }

        .current
        {
            font-weight: bold;
        }

        .table
        {
            background-position: bottom;
            background-size: 100% 2px;
            background-repeat: no-repeat;
        }

        .table-addendum
        {
            position: absolute;
            z-index: 1;
            display: none;
            white-space: pre;
            transform: translate(0.25rem, calc(-50% + 0.5em));
            color: var(--element-background-color);
            background: var(--text-color);
            border: 1px solid var(--border-color);
            border-radius: 0.5rem;
            padding: 0.25rem;
            margin-left: 0.25rem;
        }

        .table-addendum::before {
            content: " ";
            position: absolute;
            top: 50%;
            transform: translate(-110%, -50%);
            line-height: 0;
            border-width: 0.5rem;
            border-style: solid;
            border-color: transparent var(--text-color) transparent transparent;
        }

        .table:hover .table-addendum
        {
            display: inline-block;
        }

        #main
        {
            grid-area: main;
            padding: 0.5rem;
        }

        #controls
        {
            /* When a page will be scrolled horizontally due to large table size, keep controls in place. */
            position: sticky;
            left: 0;
        }

        /* Otherwise Webkit based browsers will display ugly border on focus. */
        textarea, input, button
        {
            outline: none;
            border: none;
            color: var(--text-color);
        }

        .monospace
        {
            /* Prefer fonts that have full hinting info. This is important for non-retina displays.
               Also I personally dislike "Ubuntu" font due to the similarity of 'r' and 'г' (it looks very ignorant). */
            font-family: DejaVu Sans Mono, Liberation Mono, MonoLisa, Consolas, monospace;
        }

        .shadow
        {
            box-shadow: 0 0 1rem var(--shadow-color);
        }

        input, textarea
        {
            border: 1px solid var(--border-color);
            /* The font must be not too small (to be inclusive) and not too large (it's less practical and make general feel of insecurity) */
            font-size: 11pt;
            padding: 0.25rem;
            background-color: var(--element-background-color);
        }

        input:focus, textarea:focus
        {
            color: var(--text-color-active);
        }

        #query
        {
            display: block;
            /* Make enough space for even big queries. */
            height: 20vh;
            /* Keeps query text-area's width full screen even when user adjusting the width of the query box. */
            width: 100%;
            min-width: 100%;
            max-width: 100%;
            margin-top: -1px; /* To compensate the border. */
            contain: strict; /* Less input lag even if the table is large. */
        }

        #inputs
        {
            white-space: nowrap;
        }

        #url
        {
            width: 70%;
        }

        #user, #password
        {
            width: calc(15% + 1px);
            margin-left: -1px;
        }

        input.ok
        {
            color: var(--element-color-ok);
        }

        input.fail
        {
            background-color: var(--element-background-color-fail);
        }

        #run_div
        {
            margin-top: 0.5rem;
            display: grid;
            place-items: center;
            gap: 1rem;
            grid-template-columns: auto auto auto 1fr auto auto;
            grid-template-areas: "run hint hourglass progress-bar progress-text theme";
        }

        #run
        {
            grid-area: run;
            user-select: none;
            color: var(--button-text-color);
            background-color: var(--button-color);
            padding: 0.25rem 1rem;
            cursor: pointer;
            font-weight: bold;
            font-size: 100%; /* Otherwise button element will have a lower font size. */
        }

        #run:hover, #run:focus
        {
            color: var(--button-active-text-color);
            background-color: var(--button-active-color);
        }

        #hint
        {
            grid-area: hint;
            color: var(--misc-text-color);
        }

        #hourglass
        {
            grid-area: hourglass;
            display: none;
            font-size: 110%;
            color: #888;
            animation: hourglass-animation 2s linear infinite;
        }

        #check-mark
        {
            grid-area: hourglass;
            display: none;
            font-size: 110%;
            color: #080;
        }

        #stats
        {
            grid-area: progress-text;
            color: var(--misc-text-color);
        }

        #stats b
        {
            color: var(--progress-color);
        }

        #progress
        {
            display: none;
            width: 100%;
            color: var(--misc-text-color);

            grid-area: progress-bar;
            background: linear-gradient(to right,
                transparent 0%,
                var(--progress-color) var(--progress),
                transparent var(--progress));
        }

        #theme
        {
            grid-area: theme;
        }

        #toggle-light, #toggle-dark
        {
            cursor: pointer;
            user-select: none;
        }

        [data-theme="light"] #toggle-light, [data-theme="dark"] #toggle-dark
        {
            display: none;
        }

        #data_div
        {
            margin-top: 1rem;
            background-color: var(--element-background-color);
        }

        #data_div.fixed
        {
            width: fit-content;
        }

        #data-table
        {
            width: 100%;
            border-spacing: 0;
            background-color: var(--element-background-color);
        }

        #data-table.fixed
        {
            table-layout: fixed;
            width: var(--table-width);
         }

        /* Will be displayed when user specified custom format. */
        #data-unparsed
        {
            width: fit-content;
            background-color: var(--element-background-color);
            margin-top: 0rem;
            padding: 0.25rem 0.5rem;
            display: none;
        }

        td
        {
            background-color: var(--element-background-color);
            /* For wide tables any individual column will be no more than 50% of page width. */
            max-width: 50vw;
            /* The content is cut unless you hover. */
            overflow: hidden;
            text-overflow: ellipsis;
            padding: 0.25rem 0.5rem;
            border: 1px solid var(--border-color);
            white-space: pre;
            vertical-align: top;
        }

        td u
        {
            text-decoration-color: var(--misc-text-color);
        }

        .right
        {
            text-align: right;
        }

        th
        {
            position: sticky;
            top: -0.5rem;
            padding: 0.25rem 0.5rem;
            text-align: center;
            background-color: var(--table-header-color);
            border: 1px solid var(--border-color);
        }

        th .name {
            display: block;
            overflow: hidden;
            text-overflow: ellipsis;
        }

        .type-hint
        {
            display: none;
            position: absolute;
            user-select: none;
            right: 0;
            top: -1.25em;
            font-size: 75%;
            font-weight: normal;
            color: var(--misc-text-color);
            z-index: 1;
            background: var(--element-background-color);
        }

        th:hover .type-hint
        {
            display: inline-block;
        }

        .resizer {
            position: absolute;
            top: 0;
            right: -0.3rem;
            width: 0.5rem;
            cursor: col-resize;
            user-select: none;
            height: 100%;
        }

        .resizer:hover, .resizer-dragging
        {
            border-radius: 0.25rem;
            background-color: var(--border-color-selected);
        }

        /* The row under mouse pointer is highlight for better legibility. */
        tr:hover
        {
            filter: var(--table-hover-filter);
        }

        .tr-selected
        {
            filter: var(--table-hover-filter);
        }

        #error
        {
            background: var(--error-color);
            white-space: pre-wrap;
            padding: 0.5rem 1rem;
            display: none;
        }

        .td-selected
        {
            white-space: pre-wrap;
            max-width: none;
            outline: 2px solid var(--border-color-selected);
        }

        td.empty-result
        {
            text-align: center;
            vertical-align: middle;
        }

        .row-number
        {
            width: 1%;
            text-align: right;
            background-color: var(--table-header-color);
            color: var(--misc-text-color);
        }

        div.empty-result
        {
            opacity: 10%;
            font-size: 7vw;
            font-family: Liberation Sans, DejaVu Sans, sans-serif;
        }

        @keyframes hourglass-animation {
            0% { transform: none; }
            25% { transform: rotate(180deg); }
            50% { transform: rotate(180deg); }
            75% { transform: rotate(360deg); }
            100% { transform: rotate(360deg); }
        }

        a, a:visited
        {
            color: var(--link-color);
            text-decoration: none;
        }

        #graph
        {
            display: none;
            margin: auto;
        }

        /* This is for graph in svg */
        text
        {
            font-size: 14px;
            fill: var(--text-color);
        }

        .node rect
        {
            fill: var(--element-background-color);
            filter: drop-shadow(.2rem .2rem .2rem var(--shadow-color));
            stroke: var(--border-color);
        }

        .edgePath path
        {
            stroke: var(--text-color);
        }

        marker
        {
            fill: var(--text-color);
        }

        #logo
        {
            fill: var(--logo-color);
        }

        #cloud-logo
        {
            color: transparent;
            text-shadow: 0 0 0 var(--logo-color);
            font-size: 10vw;
            display: block;
        }

        #logo-container
        {
            text-align: center;
            margin-top: 5em;
            line-height: 0.75;
        }

        #chart
        {
            border: 1px solid var(--border-color);
            background-color: var(--element-background-color);
            filter: drop-shadow(.2rem .2rem .2rem var(--shadow-color));
            display: none;
            height: 70vh;
        }

        #url_status
        {
            position: absolute;
            display: inline-block;
            font-size: 11pt;
            padding: 0.25rem;
            transform: translateX(-100%);
            color: transparent;
            user-select: none;
        }

        #url_status.ok
        {
            color: #0F0;
        }

        #url_status.fail
        {
            color: #F00;
        }

        #server_info
        {
            color: var(--misc-text-color);
        }

        /* Prevents a different color when Chromium auto-fills inputs from the list. */
        input:-webkit-autofill,
        input:-webkit-autofill:hover,
        input:-webkit-autofill:focus,
        input:-webkit-autofill:active
        {
            transition: background-color 600000s 0s, color 600000s 0s;
        }

        /* This is for charts (uPlot), Copyright (c) 2022 Leon Sorokin, MIT License, https://github.com/leeoniya/uPlot/ */
        .u-wrap {position: relative;user-select: none;}
        .u-over, .u-under, .u-axis {position: absolute;}
        .u-under {overflow: hidden;}
        .uplot canvas {display: block;position: relative;width: 100%;height: 100%;}
        .u-legend {font-family: Liberation Mono, DejaVu Sans Mono, MonoLisa, Consolas, monospace; border-spacing: 0; border-collapse: collapse;}
        .u-inline {display: block;}
        .u-inline * {display: inline-block;}
        .u-legend th {font-weight: 600;}
        .u-legend th > * {vertical-align: middle;display: inline-block;}
        .u-legend td { min-width: 13em; }
        .u-legend .u-marker {width: 1em;height: 1em;margin-right: 4px;background-clip: padding-box !important;}
        .u-inline.u-live th::after {content: ":";vertical-align: middle;}
        .u-inline:not(.u-live) .u-value {display: none;}
        .u-series > * {padding: 4px;}
        .u-series th {cursor: pointer;}
        .u-legend .u-off > * {opacity: 0.3;}
        .u-select {background: rgba(0,0,0,0.07);position: absolute;pointer-events: none;}
        .u-cursor-x, .u-cursor-y {position: absolute;left: 0;top: 0;pointer-events: none;will-change: transform;z-index: 100;}
        .u-hz .u-cursor-x, .u-vt .u-cursor-y {height: 100%;border-right: 1px dashed #607D8B;}
        .u-hz .u-cursor-y, .u-vt .u-cursor-x {width: 100%;border-bottom: 1px dashed #607D8B;}
        .u-cursor-pt {position: absolute;top: 0;left: 0;border-radius: 50%;border: 0 solid;pointer-events: none;will-change: transform;z-index: 100;/*this has to be !important since we set inline "background" shorthand */background-clip: padding-box !important;}
        .u-axis.u-off, .u-select.u-off, .u-cursor-x.u-off, .u-cursor-y.u-off, .u-cursor-pt.u-off {display: none;}
    </style>
</head>

<body>
    <div id="menu">
        <div id="databases-icon" class="menu-icon">
            <svg id="databases-toggle" viewBox="0 0 9 9"><path d="M0,0 h1 v8 h-1 z"/><path d="M2,0 h1 v8 h-1 z"/><path d="M4,0 h1 v8 h-1 z"/><path d="M6,0 h1 v8 h-1 z"/><path d="M8,3.25 h1 v1.5 h-1 z"/></svg>
        </div>
        <div id="databases"></div>
        <!-- https://github.com/logos -->
        <div id="github-icon" class="menu-icon">
            <a href="https://github.com/ClickHouse/ClickHouse/">
                <svg id="github-icon-light" viewBox="0 0 98 96" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" clip-rule="evenodd" d="M48.854 0C21.839 0 0 22 0 49.217c0 21.756 13.993 40.172 33.405 46.69 2.427.49 3.316-1.059 3.316-2.362 0-1.141-.08-5.052-.08-9.127-13.59 2.934-16.42-5.867-16.42-5.867-2.184-5.704-5.42-7.17-5.42-7.17-4.448-3.015.324-3.015.324-3.015 4.934.326 7.523 5.052 7.523 5.052 4.367 7.496 11.404 5.378 14.235 4.074.404-3.178 1.699-5.378 3.074-6.6-10.839-1.141-22.243-5.378-22.243-24.283 0-5.378 1.94-9.778 5.014-13.2-.485-1.222-2.184-6.275.486-13.038 0 0 4.125-1.304 13.426 5.052a46.97 46.97 0 0 1 12.214-1.63c4.125 0 8.33.571 12.213 1.63 9.302-6.356 13.427-5.052 13.427-5.052 2.67 6.763.97 11.816.485 13.038 3.155 3.422 5.015 7.822 5.015 13.2 0 18.905-11.404 23.06-22.324 24.283 1.78 1.548 3.316 4.481 3.316 9.126 0 6.6-.08 11.897-.08 13.526 0 1.304.89 2.853 3.316 2.364 19.412-6.52 33.405-24.935 33.405-46.691C97.707 22 75.788 0 48.854 0z" fill="#24292f"/></svg>
                <svg id="github-icon-dark" viewBox="0 0 98 96" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" clip-rule="evenodd" d="M48.854 0C21.839 0 0 22 0 49.217c0 21.756 13.993 40.172 33.405 46.69 2.427.49 3.316-1.059 3.316-2.362 0-1.141-.08-5.052-.08-9.127-13.59 2.934-16.42-5.867-16.42-5.867-2.184-5.704-5.42-7.17-5.42-7.17-4.448-3.015.324-3.015.324-3.015 4.934.326 7.523 5.052 7.523 5.052 4.367 7.496 11.404 5.378 14.235 4.074.404-3.178 1.699-5.378 3.074-6.6-10.839-1.141-22.243-5.378-22.243-24.283 0-5.378 1.94-9.778 5.014-13.2-.485-1.222-2.184-6.275.486-13.038 0 0 4.125-1.304 13.426 5.052a46.97 46.97 0 0 1 12.214-1.63c4.125 0 8.33.571 12.213 1.63 9.302-6.356 13.427-5.052 13.427-5.052 2.67 6.763.97 11.816.485 13.038 3.155 3.422 5.015 7.822 5.015 13.2 0 18.905-11.404 23.06-22.324 24.283 1.78 1.548 3.316 4.481 3.316 9.126 0 6.6-.08 11.897-.08 13.526 0 1.304.89 2.853 3.316 2.364 19.412-6.52 33.405-24.935 33.405-46.691C97.707 22 75.788 0 48.854 0z" fill="#fff"/></svg>
            </a>
        </div>
    </div>
    <div id="main">
        <form id="controls">
            <div id="inputs">
                <input name="url" spellcheck="false" class="monospace shadow" id="url" type="url" value="" placeholder="url" /><span id="url_status"><span id="server_info"></span>●</span><input name="user" spellcheck="false" class="monospace shadow" id="user" type="text" value="default" placeholder="user" /><input name="password" class="monospace shadow" id="password" type="password" placeholder="password" />
            </div>
            <div id="query_div"><textarea autofocus spellcheck="false" autocorrect="false" autocomplete="false" data-gramm="false" class="monospace shadow" id="query"></textarea></div>
            <div id="run_div">
                <button type="submit" class="shadow" id="run">Run</button>
                <div id="hint">&nbsp;(Ctrl/Cmd+Enter)</div>
                <div id="hourglass">⧗</div>
                <div id="check-mark">✔</div>
                <div id="progress"></div>
                <div id="stats"></div>
                <div id="theme"><span id="toggle-dark">🌘</span><span id="toggle-light">☀️</span></div>
            </div>
        </form>
        <div id="data_div">
            <table class="monospace shadow" id="data-table"></table>
            <pre class="monospace shadow" id="data-unparsed"></pre>
        </div>
        <div id="chart"></div>
        <svg id="graph" fill="none"></svg>
        <p id="error" class="monospace shadow">
        </p>
        <p id="logo-container">
            <a href="https://clickhouse.com/">
            <svg id="logo" width="50%" viewBox="0 0 180 28" version="1.1" xmlns="http://www.w3.org/2000/svg"><title>Powered by ClickHouse</title><g transform="translate(21, 0)"><path d="M9.76,20.95 C8.43,20.97 7.11,20.73 5.87,20.26 C4.72,19.80 3.67,19.11 2.81,18.22 C1.90,17.32 1.20,16.25 0.74,15.07 C-0.24,12.29 -0.24,9.27 0.74,6.49 C1.67,4.07 3.61,2.16 6.08,1.26 C7.35,0.79 8.71,0.55 10.07,0.57 C11.16,0.54 12.24,0.67 13.30,0.93 C14.15,1.16 14.98,1.47 15.78,1.87 L15.03,4.17 C14.33,3.70 13.57,3.32 12.78,3.03 C11.83,2.71 10.83,2.56 9.82,2.58 C7.96,2.56 6.18,3.35 4.96,4.74 C4.30,5.50 3.79,6.38 3.47,7.33 C3.08,8.46 2.89,9.65 2.91,10.85 C2.90,12.02 3.07,13.18 3.43,14.29 C3.73,15.23 4.21,16.09 4.86,16.84 C5.46,17.52 6.21,18.06 7.05,18.41 C7.95,18.78 8.92,18.96 9.90,18.96 C10.80,18.96 11.68,18.82 12.53,18.55 C13.34,18.27 14.12,17.91 14.85,17.47 L15.57,19.20 C14.87,19.72 14.09,20.13 13.25,20.40 C12.13,20.78 10.95,20.97 9.76,20.95 L9.76,20.95 Z"></path><polygon points="21.20 0 21.20 20.67 18.73 20.67 18.73 0 21.20 0"></polygon><path d="M26.51,4.15 C26.09,4.18 25.68,4.03 25.38,3.74 C25.08,3.46 24.92,3.05 24.94,2.64 C24.92,2.23 25.07,1.83 25.35,1.52 C25.99,0.92 26.99,0.92 27.63,1.52 C27.92,1.82 28.07,2.23 28.06,2.64 C28.07,3.05 27.92,3.44 27.63,3.72 C27.33,4.01 26.93,4.16 26.51,4.15 L26.51,4.15 Z M27.75,6.31 L27.75,20.56 L25.31,20.56 L25.31,6.31 L27.77,6.31 L27.75,6.31 Z"></path><path d="M38.20,20.95 C37.23,20.96 36.27,20.78 35.36,20.44 C34.50,20.12 33.72,19.62 33.07,18.98 C32.40,18.30 31.88,17.51 31.54,16.63 C30.78,14.61 30.78,12.39 31.54,10.36 C31.89,9.48 32.43,8.68 33.11,8.02 C33.80,7.36 34.62,6.85 35.53,6.53 C36.46,6.19 37.45,6.02 38.44,6.02 C39.23,6.02 40.02,6.11 40.78,6.29 C41.40,6.43 41.99,6.67 42.52,7.00 L41.73,9.14 C41.23,8.76 40.68,8.44 40.10,8.18 C39.49,7.95 38.85,7.84 38.20,7.86 C37.57,7.85 36.95,7.97 36.38,8.20 C35.80,8.44 35.28,8.80 34.87,9.26 C34.42,9.77 34.07,10.36 33.85,10.99 C33.58,11.78 33.45,12.61 33.46,13.44 C33.36,14.92 33.85,16.38 34.83,17.51 C35.81,18.45 37.15,18.94 38.53,18.87 C39.72,18.85 40.89,18.48 41.88,17.81 L42.44,19.61 C41.88,20.02 41.26,20.33 40.60,20.52 C39.82,20.79 39.01,20.93 38.20,20.95 L38.20,20.95 Z"></path><path d="M47.94,0 L47.94,20.67 L45.44,20.67 L45.44,0 L47.94,0 Z M54.35,20.67 L48.27,13.01 L54.27,6.33 L56.83,6.33 L50.90,12.72 L57.24,20.67 L54.35,20.67 L54.35,20.67 Z"></path><polygon points="73.11 11.87 62.77 11.87 62.77 20.65 60.10 20.65 60.10 0.85 62.77 0.85 62.77 9.89 73.11 9.89 73.11 0.85 75.78 0.85 75.78 20.67 73.11 20.67"></polygon><path d="M86.30,20.95 C85.38,20.95 84.47,20.78 83.61,20.44 C82.78,20.12 82.01,19.63 81.38,19.00 C80.72,18.32 80.21,17.53 79.87,16.65 C79.12,14.60 79.12,12.35 79.87,10.30 C80.21,9.43 80.72,8.64 81.38,7.98 C82.02,7.35 82.78,6.86 83.61,6.53 C84.47,6.20 85.38,6.02 86.30,6.02 C87.23,6.02 88.15,6.20 89.01,6.53 C89.86,6.86 90.62,7.35 91.27,7.98 C91.94,8.64 92.47,9.43 92.82,10.30 C93.59,12.35 93.59,14.60 92.82,16.65 C92.50,17.52 92.00,18.32 91.37,19.00 C90.73,19.63 89.96,20.12 89.12,20.44 C88.22,20.79 87.27,20.96 86.30,20.95 Z M86.30,19.10 C86.88,19.10 87.46,18.99 88.00,18.79 C88.56,18.59 89.07,18.25 89.47,17.81 C89.93,17.31 90.28,16.71 90.50,16.06 C90.78,15.23 90.91,14.35 90.89,13.48 C90.92,12.60 90.78,11.72 90.50,10.89 C90.28,10.25 89.93,9.66 89.47,9.16 C89.07,8.72 88.56,8.39 88.00,8.18 C87.46,7.98 86.88,7.88 86.30,7.88 C85.74,7.88 85.18,7.98 84.65,8.18 C84.10,8.39 83.61,8.73 83.22,9.16 C82.77,9.66 82.42,10.25 82.21,10.89 C81.94,11.73 81.81,12.60 81.84,13.48 C81.81,14.35 81.94,15.23 82.21,16.06 C82.42,16.71 82.76,17.31 83.22,17.81 C83.61,18.25 84.10,18.58 84.65,18.79 C85.18,18.99 85.74,19.10 86.30,19.10 L86.30,19.10 Z"></path><path d="M104.21,20.12 C103.26,20.71 102.14,21.00 101.01,20.95 C99.79,21.00 98.61,20.56 97.74,19.73 C96.83,18.70 96.38,17.34 96.50,15.98 L96.50,6.31 L98.98,6.31 L98.98,15.98 C98.97,16.47 99.04,16.95 99.19,17.41 C99.30,17.76 99.50,18.07 99.77,18.32 C100.01,18.55 100.31,18.72 100.64,18.81 C100.99,18.91 101.35,18.96 101.71,18.96 C102.65,19.00 103.58,18.73 104.34,18.18 C105.05,17.63 105.63,16.94 106.03,16.14 L106.03,6.31 L108.49,6.31 L108.49,20.56 L106.32,20.56 L106.10,18.00 L106.10,18.00 C105.68,18.86 105.03,19.60 104.21,20.12 L104.21,20.12 Z"></path><path d="M116.42,20.95 C115.80,20.99 115.17,20.99 114.55,20.95 C114.09,20.89 113.62,20.80 113.17,20.69 C112.81,20.60 112.46,20.48 112.11,20.34 C111.83,20.22 111.55,20.07 111.29,19.91 L112.09,17.88 C112.27,18.02 112.47,18.15 112.67,18.26 C112.97,18.43 113.28,18.57 113.60,18.69 C114.01,18.85 114.43,18.97 114.86,19.06 C115.38,19.16 115.90,19.20 116.42,19.20 C117.26,19.26 118.09,19.04 118.79,18.59 C119.33,18.19 119.63,17.56 119.60,16.90 C119.62,16.34 119.38,15.80 118.94,15.45 C118.24,14.98 117.47,14.63 116.64,14.43 L114.86,13.84 C114.33,13.66 113.82,13.39 113.36,13.07 C112.91,12.76 112.55,12.35 112.28,11.89 C111.99,11.34 111.84,10.72 111.87,10.10 C111.79,8.92 112.33,7.79 113.29,7.10 C114.46,6.39 115.83,6.05 117.20,6.13 C118.07,6.11 118.94,6.19 119.79,6.37 C120.38,6.49 120.97,6.68 121.52,6.92 L120.95,8.96 C120.47,8.73 119.99,8.52 119.50,8.34 C118.79,8.11 118.05,8.00 117.31,8.02 C116.53,7.98 115.76,8.13 115.05,8.45 C114.47,8.75 114.14,9.37 114.20,10.01 C114.20,10.33 114.29,10.63 114.47,10.89 C114.66,11.15 114.90,11.36 115.18,11.52 C115.50,11.72 115.85,11.87 116.21,11.99 L117.45,12.36 L119.13,12.91 C119.66,13.10 120.16,13.36 120.61,13.70 C121.60,14.41 122.15,15.56 122.08,16.76 C122.12,17.46 121.95,18.17 121.61,18.79 C121.30,19.33 120.86,19.79 120.35,20.14 C119.80,20.50 119.19,20.76 118.55,20.91 C117.84,21.01 117.13,21.02 116.42,20.95 Z"></path><path d="M136.37,19.42 C135.76,19.91 135.06,20.28 134.31,20.50 C133.39,20.80 132.43,20.95 131.47,20.93 C129.48,21.04 127.53,20.30 126.14,18.89 C124.81,17.36 124.15,15.38 124.28,13.38 C124.26,12.31 124.43,11.26 124.79,10.26 C125.11,9.39 125.59,8.60 126.22,7.92 C126.79,7.29 127.50,6.79 128.29,6.45 C129.13,6.11 130.03,5.94 130.93,5.94 C131.84,5.93 132.74,6.10 133.58,6.45 C134.35,6.77 135.02,7.28 135.53,7.94 C136.06,8.66 136.42,9.50 136.56,10.38 C136.75,11.50 136.75,12.64 136.56,13.76 L126.72,13.76 C126.83,17.17 128.46,18.87 131.62,18.87 C132.38,18.89 133.13,18.77 133.85,18.53 C134.49,18.30 135.10,18.00 135.67,17.63 L136.37,19.42 Z M131.02,7.73 C130.01,7.72 129.05,8.12 128.35,8.83 C127.52,9.75 127.03,10.91 126.94,12.13 L134.33,12.13 C134.53,10.97 134.25,9.78 133.56,8.81 C132.93,8.09 131.99,7.69 131.02,7.73 Z"></path></g></svg>
            </a>
            <a id="cloud-logo" href="https://clickhouse.cloud/">☁</a>
        </p>
    </div>
</body>

<script type="text/javascript">

/// Incremental request number. When response is received,
/// if its request number does not equal to the current request number, response will be ignored.
/// This is to avoid race conditions.
let request_num = 0;

/// Save query in history only if it is different.
let previous_query = '';

/// Start of the last query
let last_query_start = 0;

const current_url = new URL(window.location);
const opened_locally = location.protocol == 'file:';

/// Run query instantly after page is loaded if the run parameter is present.
const run_immediately = current_url.searchParams.has("run");

let url_elem = document.getElementById('url');
let user_elem = document.getElementById('user');
let password_elem = document.getElementById('password');

if (url_elem.value == '') {
    const server_address = current_url.searchParams.get('url');
    if (server_address) {
        url_elem.value = server_address;
    } else if (!opened_locally) {
        /// Substitute the address of the server where the page is served.
        url_elem.value = location.origin;
    } else {
        url_elem.value = 'http://localhost:8123/';
    }
}

/// Substitute username if it's specified in the query string
const user_from_url = current_url.searchParams.get('user');
if (user_from_url) {
    user_elem.value = user_from_url;
}

const pass_from_url = current_url.searchParams.get('password');
if (pass_from_url) {
    password_elem.value = pass_from_url;
    /// Browsers don't allow manipulating history for the 'file:' protocol.
    if (!opened_locally) {
        let replaced_pass = current_url.searchParams;
        replaced_pass.delete('password');
        window.history.replaceState(null, '',
            window.location.origin + window.location.pathname + '?'
            + replaced_pass.toString() + window.location.hash);
    }
}

async function ping(url) {
    try {
        let response = await fetch(new URL(url).origin + "?query", { method: 'OPTIONS', headers: { 'Authorization': 'never' } });
        return response.ok;
    } catch (e) {
        return false;
    }
}

let ping_request_num = 0;
function checkURL() {
    ++ping_request_num;
    const current_ping_request_num = ping_request_num;
    let elem = document.getElementById('url_status');
    elem.className = '';
    document.getElementById('server_info').innerText = '';
    ping(url_elem.value).then(status => {
        if (current_ping_request_num == ping_request_num) {
            elem.className = status ? 'ok' : 'fail';
            if (status) {
                checkCredentials();
            }
        }
    });
};

url_elem.addEventListener('input', checkURL);
checkURL();

async function getServerStatus(server_address, user, password) {
    let url = server_address +
        (server_address.indexOf('?') >= 0 ? '&' : '?') +
        'add_http_cors_header=1&default_format=JSONEachRow';
    if (user) url += '&user=' + encodeURIComponent(user);
    if (password) url += '&password=' + encodeURIComponent(password);
    try {
        let response = await fetch(url, { method: "POST", body: "SELECT version() AS v, uptime() AS t", headers: { 'Authorization': 'never' } });
        if (!response.ok) return false;
        json = await response.json();
        return json;
    } catch (e) {
        return false;
    }
}

let load_databases_request_num = 0;
async function loadDatabases(server_address, user, password) {
    ++load_databases_request_num;
    const current_load_databases_request_num = load_databases_request_num;
    let url = server_address +
        (server_address.indexOf('?') >= 0 ? '&' : '?') +
        'add_http_cors_header=1&default_format=JSON';
    if (user) url += '&user=' + encodeURIComponent(user);
    if (password) url += '&password=' + encodeURIComponent(password);

    let response = await fetch(url, {
        method: "POST",
        body: `SELECT database, database = currentDatabase() AS current
            FROM system.databases WHERE database != 'INFORMATION_SCHEMA'
            ORDER BY
                database = 'information_schema',
                database = 'system',
                database != 'default',
                database`,
        headers: { 'Authorization': 'never' } });
    if (!response.ok) return false;
    json = await response.json();

    if (current_load_databases_request_num != load_databases_request_num) return false;

    let databases_elem = document.getElementById('databases');
    while (databases_elem.firstChild) databases_elem.removeChild(databases_elem.firstChild);

    for (let elem of json.data) {
        let database = document.createElement('div');
        database.className = 'database';
        let database_link = document.createElement('button');
        if (elem.current) database_link.className = 'current';
        database_link.append(elem.database);
        database.appendChild(database_link);
        let tables = document.createElement('div');
        tables.className = 'tables';
        database.appendChild(tables);
        databases_elem.appendChild(database);

        database_link.addEventListener('click', e => {
            if (!database.dataset['opened']) {
                loadTables(server_address, user, password, elem.database, database).then(() => database.dataset['opened'] = 1);
            } else {
                delete database.dataset['opened'];
                while (tables.firstChild) tables.removeChild(tables.firstChild);
            }
        });
    }
}

let load_tables_request_num = 0;
async function loadTables(server_address, user, password, database, container) {
    ++load_tables_request_num;
    const current_load_tables_request_num = load_tables_request_num;
    let url = server_address +
        (server_address.indexOf('?') >= 0 ? '&' : '?') +
        'add_http_cors_header=1&default_format=JSON';
    if (user) url += '&user=' + encodeURIComponent(user);
    if (password) url += '&password=' + encodeURIComponent(password);
    url += '&param_database=' + encodeURIComponent(database);

    let response = await fetch(url, {
        method: "POST",
        body: `SELECT table, engine, total_rows, total_bytes FROM system.tables WHERE database = {database:String}
            ORDER BY table LIKE '.inner%', table`,
        headers: { 'Authorization': 'never' } });
    if (!response.ok) return false;
    json = await response.json();

    if (current_load_tables_request_num != load_tables_request_num) return false;

    container.parentElement.querySelector('.current').classList.remove('current');
    container.firstChild.classList.add('current');
    let tables_elem = container.querySelector('.tables');
    while (tables_elem.firstChild) tables_elem.removeChild(tables_elem.firstChild);

    let max_total_bytes = Math.max(...json.data.map(elem => +elem.total_bytes));

    for (let elem of json.data) {
        let table = document.createElement('div');
        table.className = 'table';
        let table_link = document.createElement('button');
        table_link.append(elem.table);

        if (max_total_bytes && +elem.total_bytes) {
            const percent = Number(100 * +elem.total_bytes / max_total_bytes).toFixed(2);
            table.style.backgroundImage = `linear-gradient(to right,
                var(--progress-color) 0%,
                var(--progress-color) ${percent}%,
                transparent ${percent}%)`;
        }

        let table_addendum = document.createElement('span');
        table_addendum.className = 'table-addendum';

        let addendum = '';
        if (+elem.total_bytes) addendum += formatReadableBytes(elem.total_bytes) + '\n';
        if (+elem.total_rows) addendum += formatReadableRows(elem.total_rows) + ' rows\n';
        addendum += elem.engine;
        table_addendum.append(addendum);

        table.appendChild(table_link);
        table.appendChild(table_addendum);
        tables_elem.appendChild(table);

        table.addEventListener('click', e => {
            const value = `SELECT * FROM "${database}"."${elem.table}" LIMIT 100`;
            if (query_area.value == value) {
                /// Double click will run the query.
                post();
            } else {
                query_area.value = value;
            }
        });
    }

    if (json.data.length == 0) {
        let table = document.createElement('div');
        table.className = 'no-tables';
        table.append('no tables');
        tables_elem.append(table);
    }
}

let databases_toggle = document.getElementById('databases-toggle');
databases_toggle.addEventListener('click', e => {
    if (!databases_toggle.dataset['opened']) {
        loadDatabases(url_elem.value, user_elem.value, password_elem.value).then(() => databases_toggle.dataset['opened'] = 1);
    } else {
        delete databases_toggle.dataset['opened'];
        let databases_elem = document.getElementById('databases');
        while (databases_elem.firstChild) databases_elem.removeChild(databases_elem.firstChild);
    }
});

function formatUptime(t) {
    if (t < 60) { return t + " sec" }
    if (t < 3600) { return Number(t / 60).toFixed() + " min" }
    if (t < 86400) { return Number(t / 3600).toFixed() + " hr" }
    const days = Number(t / 86400).toFixed();
    return days + " day" + (days > 1 ? 's' : '');
}

let check_credentials_request_num = 0;
function checkCredentials() {
    ++check_credentials_request_num;
    const current_check_credentials_request_num = check_credentials_request_num;
    getServerStatus(url_elem.value, user_elem.value, password_elem.value).then(json => {
        if (current_check_credentials_request_num == check_credentials_request_num) {
            if (json) {
                [user_elem, password_elem].forEach(e => { e.classList.remove('fail'); e.classList.add('ok'); });
                document.getElementById('server_info').innerText = `v${json.v}, uptime ${formatUptime(json.t)} `;
            } else {
                [user_elem, password_elem].forEach(e => { e.classList.remove('ok'); e.classList.add('fail'); });
            }
        }
    });
}

user_elem.addEventListener('input', checkCredentials);
password_elem.addEventListener('input', checkCredentials);


function queryToColor(str) {
    let hash = 0;
    for (let i = 0; i < str.length; i++) {
        hash = ((hash << 5) - hash) + str.charCodeAt(i);
        hash = hash & hash;
    }

    /// Limited range to avoid violet colors.
    return 50 + Math.abs(hash % 200);
}

function toBase64(str) {
    const encoder = new TextEncoder();
    const bytes = encoder.encode(str);
    const binary = Array.from(bytes, byte => String.fromCharCode(byte)).join('');
    return btoa(binary);
}

function fromBase64(base64) {
    const binary = atob(base64);
    const bytes = new Uint8Array(binary.length);
    for (let i = 0; i < binary.length; i++) { bytes[i] = binary.charCodeAt(i); }
    const decoder = new TextDecoder();
    return decoder.decode(bytes);
}

let row_idx = 0;
let elapsed_ns = 0;
let incomplete_result = false;
let is_explain_graph = false;
let explain_graph = '';
let column_is_number = {};
let extremes = {};
let header = {};

const default_format = 'JSONStringsEachRowWithProgress';

let controller = null;
async function postImpl(posted_request_num, query)
{
    const user = user_elem.value;
    const password = password_elem.value;
    const server_address = url_elem.value;
    let url = server_address +
        (server_address.indexOf('?') >= 0 ? '&' : '?') +
        /// Ask server to allow cross-domain requests.
        'add_http_cors_header=1';
    if (user) url += '&user=' + encodeURIComponent(user);
    if (password) url += '&password=' + encodeURIComponent(password);
    url += '&default_format=' + default_format + '&enable_http_compression=1';

    /// Extremes only if the format is likely to be unchanged. This is imprecise.
    if (!query.match(/\bFORMAT\s+\w+/i)) url += '&extremes=1';

    last_query_start = performance.now();

    document.getElementById('check-mark').style.display = 'none';
    document.getElementById('hourglass').style.display = 'inline-block';

    clear();

    let is_table = false;
    let is_raw = false;
    let is_chart = false;
    let is_error = false;

    let response, reply, format;
    reply = '';
    try {
        controller = new AbortController();
        response = await fetch(url, { method: "POST", body: query, signal: controller.signal,
            headers: { 'Authorization': 'never' } });

        format = response.headers.get('X-ClickHouse-Format') ?? default_format;

        if (!response.ok) {
            is_error = true;
            reply = await response.text();
            if (posted_request_num != request_num) { return; }

            let has_exception = false;
            for (line of reply.split('\n')) {
                if (line.startsWith(`{"exception":`)) {
                    update(JSON.parse(line));
                    has_exception = true;
                    break;
                }
            }
            if (!has_exception) {
                renderError(reply);
            }

            document.getElementById('hourglass').style.display = 'none';
            document.documentElement.style.setProperty('--progress', '0');
            return;
        } else if (format == default_format) {
            is_table = true;
            const reader = response.body.getReader();
            const decoder = new TextDecoder();

            let buffer = '';
            while (true) {
                const { done, value } = await reader.read();
                if (posted_request_num != request_num) { return; }
                if (done) break;

                let new_content = decoder.decode(value, { stream: true });
                buffer += new_content;
                reply += new_content;

                let lines = buffer.split('\n');
                let fragment = document.createDocumentFragment();

                let cont = true;
                for (line of lines.slice(0, -1)) {
                    const data = JSON.parse(line);
                    cont = update(data, fragment);
                    if (!cont) { break; }
                }

                if (fragment.hasChildNodes()) {
                    document.getElementById('tbody').appendChild(fragment);
                }

                if (!cont)
                {
                    controller.abort();
                    break;
                }
                buffer = lines[lines.length - 1];
            }
        } else if (format == 'JSONCompactColumns') {
            is_chart = true;
            reply = await response.text();
            if (posted_request_num != request_num) { return; }
            renderChart(JSON.parse(reply));
        } else {
            is_raw = true;
            const reader = response.body.getReader();
            const decoder = new TextDecoder();

            let buffer = '';
            while (true) {
                const { done, value } = await reader.read();
                if (done) break;

                let new_content = decoder.decode(value, { stream: true });
                reply += new_content;

                if (posted_request_num != request_num) { return; }
                updateRaw(new_content);
            }
        }
    } catch (e) {
        if (posted_request_num != request_num) { return; }

        console.log(e);
        if (e instanceof TypeError) {
            reply = "Network error";
        } else if (e.name === 'AbortError') {
            reply = "Query was cancelled";
        } else {
            reply = e.toString();
        }
        renderError(reply);
        document.getElementById('hourglass').style.display = 'none';
        document.documentElement.style.setProperty('--progress', '0');
        return;
    }

    if (is_explain_graph) {
        await renderGraph();
    } else {
        if (extremes['min'] && extremes['max']) renderExtremes();
        if (is_table) transposeTableIfNeeded();
    }

    document.getElementById('check-mark').style.display = 'inline-block';
    document.getElementById('hourglass').style.display = 'none';

    if (is_raw) {
        elapsed_ns = 1e6 * (performance.now() - last_query_start);
    }

    updateProgressText();
    document.documentElement.style.setProperty('--progress', '0');
    document.documentElement.style.setProperty('--background-color-rotate', queryToColor(query));

    if (posted_request_num != request_num) { return; }

    /// The query is saved in browser history (in a state JSON object)
    /// as well as in URL fragment identifier.
    if (query != previous_query) {
        const state = {
            query: query,
            format: format,
            ok: response.ok,
            data: reply.length > 100000 ? null : reply, /// Lower than the browser's limit.
            elapsed_ns: elapsed_ns,
        };
        const title = "ClickHouse Query: " + query;

        let history_url = window.location.pathname + '?user=' + encodeURIComponent(user);
        if (run_immediately) {
            history_url += "&run=1";
        }
        if (server_address != location.origin) {
            /// Save server's address in URL if it's not identical to the address of the play UI.
            history_url += '&url=' + encodeURIComponent(server_address);
        }
        history_url += '#' + toBase64(query);

        if (previous_query == '') {
            history.replaceState(state, title, history_url);
        } else {
            history.pushState(state, title, history_url);
        }
        document.title = title;
        previous_query = query;
    }
}

async function restoreFromHistory(state) {
    clear();
    query_area.value = state.query;

    if (!state.data) return;

    let is_table = false;
    let is_raw = false;
    let is_chart = false;
    let is_error = false;

    let format = state.format;

    if (!state.ok) {
        is_error = true;

        let has_exception = false;
        for (line of state.data.split('\n')) {
            if (line.startsWith(`{"exception":`)) {
                update(JSON.parse(line));
                has_exception = true;
                break;
            }
        }
        if (!has_exception) {
            renderError(state.data);
        }

        document.getElementById('hourglass').style.display = 'none';
        document.documentElement.style.setProperty('--progress', '0');
        return;
    } else if (format == default_format) {
        is_table = true;
        let lines = state.data.split('\n');
        let fragment = document.createDocumentFragment();

        for (line of lines) {
            if (line.length == 0) continue;
            if (!update(JSON.parse(line), fragment)) { break; }
        }

        if (fragment.hasChildNodes()) {
            document.getElementById('tbody').appendChild(fragment);
        }
    } else if (format == 'JSONCompactColumns') {
        is_chart = true;
        if (posted_request_num != request_num) { return; }
        renderChart(JSON.parse(state.data));
    } else {
        is_raw = true;
        updateRaw(state.data);
    }

    if (is_explain_graph) {
        await renderGraph();
    } else {
        if (extremes['min'] && extremes['max']) renderExtremes();
        if (is_table) transposeTableIfNeeded();
    }

    document.getElementById('check-mark').style.display = 'inline-block';
    document.getElementById('hourglass').style.display = 'none';

    if (is_raw) elapsed_ns = state.elapsed_ns;

    updateProgressText();
    document.documentElement.style.setProperty('--progress', '0');
    document.documentElement.style.setProperty('--background-color-rotate', queryToColor(query));
}

let query_area = document.getElementById('query');

window.onpopstate = function(event) {
    if (!event.state) return;
    restoreFromHistory(event.state);
};

if (window.location.hash) {
    query_area.value = fromBase64(window.location.hash.substr(1));
}

let in_flight = false;
async function post()
{
    ++request_num;
    document.body.scrollTo(0, 0);
    let query = await getQueryUnderCursor();
    in_flight = true;
    let run = document.getElementById('run');
    run.innerText = 'Stop';
    await postImpl(request_num, query);
    run.blur();
    run.innerText = 'Run';
    in_flight = false;
}

async function cancel()
{
    controller.abort();
    document.getElementById('run').innerText = 'Run';
}

document.getElementById('controls').addEventListener('submit', e =>
{
    e.preventDefault();
    if (in_flight) {
        cancel();
    } else {
        post();

        if (password_elem.value) {
            const cred = new PasswordCredential({
                id: user_elem.value,
                password: password_elem.value,
                name: url_elem.value,
            });
            navigator.credentials.store(cred);
        }
    }
});

document.addEventListener('keydown', event => {
    /// Firefox has code 13 for Enter and Chromium has code 10.
    if ((event.metaKey || event.ctrlKey) && (event.keyCode == 13 || event.keyCode == 10)) {
        document.getElementById('controls').requestSubmit();
    }
});

/// Pressing Tab in textarea will increase indentation.
/// But for accessibility reasons, we will fall back to tab navigation if the user already used Tab for that.

let user_prefers_tab_navigation = false;

[...document.querySelectorAll('input')].map(elem => {
    elem.onkeydown = (e) => {
        if (e.key == 'Tab') { user_prefers_tab_navigation = true; }
    };
});

query_area.onkeydown = (e) => {
    if (e.key == 'Tab' && !event.shiftKey && !user_prefers_tab_navigation) {
        document.execCommand('insertText', false, '    ');
        e.preventDefault();
        return false;
    } else if (e.key === 'Enter' && !(event.metaKey || event.ctrlKey)) {
        // If the user presses Enter, and the previous line starts with spaces,
        // then we will insert the same number of spaces.
        const elem = e.target;
        if (elem.selectionStart !== elem.selectionEnd) {
            // If there is a selection, then we will not insert spaces.
            return;
        }
        const cursor_pos = elem.selectionStart;

        const elem_value = elem.value;
        const text_before_cursor = elem_value.substring(0, cursor_pos);
        const text_after_cursor = elem_value.substring(cursor_pos);
        const prev_lines = text_before_cursor.split('\n');
        const prev_line = prev_lines.pop();
        const lead_spaces = prev_line.match(/^\s*/)[0];
        if (!lead_spaces) {
            return;
        }

        // Add leading spaces to the current line.
        elem.value = text_before_cursor + '\n' + lead_spaces + text_after_cursor;
        elem.selectionStart = cursor_pos + lead_spaces.length + 1;
        elem.selectionEnd = elem.selectionStart;

        e.preventDefault();
        return false;
    }
};

function clearElement(id)
{
    let elem = document.getElementById(id);
    while (elem.firstChild) {
        elem.removeChild(elem.lastChild);
    }
    elem.style.display = 'none';
}

function clear()
{
    clearElement('data-table');
    clearElement('graph');
    clearElement('chart');
    clearElement('data-unparsed');
    clearElement('error');
    clearElement('progress');

    document.getElementById('data-table').classList.remove('fixed');
    document.getElementById('data_div').classList.remove('fixed');
    document.getElementById('check-mark').display = 'none';
    document.getElementById('hourglass').display = 'none';
    document.getElementById('stats').innerText = '';
    document.getElementById('logo-container').style.display = 'block';

    extremes = {};
    header = {};
    row_idx = 0;
    elapsed_ns = 0;
    incomplete_result = false;
    is_explain_graph = false;
    explain_graph = '';
}

function formatReadable(number = 0, decimals = 2, units = []) {
    const k = 1000;
    const i = number ? Math.floor(Math.log(number) / Math.log(k)) : 0;
    const unit = units[i];
    const dm = unit ? decimals : 0;
    return Number(number / Math.pow(k, i)).toFixed(dm) + unit;
}

function formatReadableBytes(bytes) {
    const units = [' B', ' KB', ' MB', ' GB', ' TB', ' PB', ' EB', ' ZB', ' YB'];

    return formatReadable(bytes, 2, units);
}

function formatReadableRows(rows) {
    const units = ['', ' thousand', ' million', ' billion', ' trillion', ' quadrillion'];

    return formatReadable(rows, 2, units);
}

function renderCell(name, value)
{
    let td = document.createElement('td');

    let is_null = (value === null);
    let is_link = false;

    /// Test: SELECT number, toString(number) AS str, number % 2 ? number : NULL AS nullable, range(number) AS arr, CAST((['hello', 'world'], [number, number % 2]) AS Map(String, UInt64)) AS map FROM numbers(10)
    let text;
    if (is_null) {
        text = 'ᴺᵁᴸᴸ';
    } else if (typeof(value) === 'object') {
        text = JSON.stringify(value);
    } else {
        text = value;

        /// If it looks like URL, create a link. This is for convenience.
        if (typeof(value) == 'string' && value.match(/^https?:\/\/\S+$/)) {
            is_link = true;
        }
    }

    let node = document.createTextNode(text);
    if (is_link) {
        let link = document.createElement('a');
        link.appendChild(node);
        link.href = text;
        link.setAttribute('target', '_blank');
        node = link;
    }

    td.className = column_is_number[name] ? 'right' : 'left';

    if (column_is_number[name]) {
        if (text.length >= 5 && text.match(/^[\d]{5,}$/)) {
            let i = 0;
            td.innerHTML = text.split('').reverse().map(c => {
                if (i > 0 && i % 3 == 0) { c = `<u>${c}<\/u>`; }; ++i; return c;
            }).reverse().join('');
            return td;
        }
    }

    td.appendChild(node);
    return td;
}

document.getElementById('query').addEventListener('focus', e => {
    current_selected_cell?.classList.remove('td-selected');
    current_selected_cell?.parentElement.classList.remove('tr-selected');
    current_selected_cell = null;
});

document.addEventListener('keydown', e => {
    let cell = current_selected_cell;
    if (!cell) { return; }

    const row = cell.parentElement;
    const table = row.parentElement;
    let cell_index = cell.cellIndex;
    let row_index = row.rowIndex;

    switch(e.key) {
        case 'ArrowUp': --row_index; break;
        case 'ArrowDown': ++row_index; break;
        case 'ArrowLeft': --cell_index; break;
        case 'ArrowRight': ++cell_index; break;
        default:
            return;
    }

    /// The first cell in each row is the row number.
    if (row_index < 0 || cell_index <= 0) { return; }

    let new_cell = table.rows[row_index]?.cells[cell_index];
    if (!new_cell) { return; }

    e.preventDefault();

    cell.classList.remove('td-selected');
    cell.parentElement.classList.remove('tr-selected');
    new_cell.classList.add('td-selected');
    new_cell.parentElement.classList.add('tr-selected');
    new_cell.scrollIntoView({ block: 'nearest' });
    current_selected_cell = new_cell;
});

function transposeTableIfNeeded()
{
    let table = document.getElementById('data-table');
    if (Object.keys(header).length == 0 || table.rows.length > 1 || table.rows[0]?.cells.length <= 5) return;

    let heads = table.tHead.childNodes;
    let values = table.rows[0]?.cells;
    let num_cols = heads.length - 1;

    let new_tbody = document.createElement('tbody');
    for (let i = 0; i < num_cols; ++i) {
        let tr = document.createElement('tr');
        let th = heads[1];
        th.className = 'right';
        th.style.width = '0';
        th.removeChild(th.querySelector('.resizer'))
        tr.appendChild(th);
        if (values) {
            let td = values[1];
            td.classList.remove('right');
            tr.appendChild(td);
        } else if (i == 0) {
            /// If the result is empty, show this fact with a style.
            let td = document.createElement('td');
            td.rowSpan = num_cols;
            td.className = 'empty-result';
            let div = document.createElement('div');
            div.appendChild(document.createTextNode("empty result"));
            div.className = 'empty-result';
            td.appendChild(div);
            tr.appendChild(td);
        }
        new_tbody.appendChild(tr);
    }

    table.removeChild(table.tHead);
    table.removeChild(table.tBodies[0]);
    table.appendChild(new_tbody);
}

function update(json, fragment) {
    if (json.progress) {
        updateProgress(json.progress);
        return true;
    } else if (json.meta) {
        appendHeader(json.meta);
        return true;
    } else if (json.row) {
        if (is_explain_graph
            || (row_idx == 0 && Object.keys(json.row).length == 1 && json.row.explain == 'digraph'
                && query_area.value.match(/^\s*EXPLAIN/i))) {
            is_explain_graph = true;
            explain_graph += json.row.explain + '\n';
            return true;
        } else {
            return appendRow(json.row, fragment);
        }
    } else if (json.min) {
        extremes['min'] = json.min;
        return true;
    } else if (json.max) {
        extremes['max'] = json.max;
        return true;
    } else if (json.exception) {
        renderError(json.exception);
        return true;
    }
}

function updateRaw(data) {
    const fragment = document.createTextNode(data);
    let container = document.getElementById('data-unparsed');
    container.appendChild(fragment);
    container.style.display = 'block';
}

function updateProgressText() {
    let progress_elem = document.getElementById('progress');
    let progress_text = '';
    if (row_idx) {
        progress_text += `${row_idx}${incomplete_result ? '+' : ''} rows in result`;
        if (elapsed_ns) progress_text += ', ';
    }
    if (elapsed_ns) { progress_text += `${(elapsed_ns / 1e9).toFixed(2)} sec.`; }
    progress_elem.innerText = progress_text;
    progress_elem.style.display = 'block';
}

function updateProgress(progress) {
    let stats = document.getElementById('stats');
    let progress_elem = document.getElementById('progress');

    const rows = +progress.read_rows;
    const bytes = +progress.read_bytes;
    const total_rows = +progress.total_rows_to_read;
    elapsed_ns = +progress.elapsed_ns;

    let formatted_rows = formatReadableRows(rows);
    let formatted_bytes = formatReadableBytes(bytes);

    const rps = rows * 1e9 / elapsed_ns;
    const bps = bytes * 1e9 / elapsed_ns;

    let formatted_rps = formatReadableRows(rps) + '/sec';
    let formatted_bps = formatReadableBytes(bps) + '/sec';

    if (rows >= 1e11) { formatted_rows = `<b>${formatted_rows}<\/b>`; }
    if (bytes >= 1e12) { formatted_bytes = `<b>${formatted_bytes}<\/b>`; }
    if (rps >= 1e10) { formatted_rps = `<b>${formatted_rps}<\/b>`; }
    if (bps >= 1e10) { formatted_bps = `<b>${formatted_bps}<\/b>`; }

    let text = '';

    if (total_rows) { text += (100 * Math.min(1.0, rows / total_rows)).toFixed(1) + '%, '; }

    text += `Read ${formatted_rows} rows, ${formatted_bytes}`;

    if (rps > 1e6) { text += ` (${formatted_rps}, ${formatted_bps})`; }

    stats.innerHTML = text;

    updateProgressText();

    document.documentElement.style.setProperty('--progress',
        rows && total_rows ? (100 * rows / total_rows) + '%' : '0');
}

function changeTableLayout() {
    const table = document.getElementById('data-table');
    const headers = table.querySelectorAll('th');

    document.getElementById('data-table').style.setProperty('--table-width', `${table.offsetWidth}px`);

    columnWidths = [];
    headers.forEach(header => {
        columnWidths.push(header.offsetWidth);
    });

    table.classList.add('fixed');
    document.getElementById('data_div').classList.add('fixed');

    headers.forEach((header, index) => {
        header.style.width = `${columnWidths[index]}px`;
    });
}


function appendHeaderResizer(header) {
    let drag_state = {
        elem: null,
        is_dragging: true,
        offset_x: null,
        offset_width: null
    };

    const start = (e) => {
        if (e.button !== 0) { return; }

        changeTableLayout();
        drag_state.offset_x = e.clientX;
        drag_state.offset_width = header.offsetWidth;
        drag_state.is_dragging = true;
        drag_state.elem = e.target;
        drag_state.elem.classList.add('resizer-dragging');

        document.addEventListener('pointermove', move);
        document.addEventListener('pointerup', stop);
        document.addEventListener('pointercancel', stop);
    }

    const move = (e) => {
        if (!drag_state.is_dragging) { return; }

        const dx = e.clientX - drag_state.offset_x;
        header.style.width = `${drag_state.offset_width + dx}px`;
        if(dx < 0) {
            const table = document.getElementById('data-table');
            table.style.setProperty('--table-width', `${table.offsetWidth + dx}px`)
        };
    }

    const stop = (e) => {
        drag_state.is_dragging = false;
        drag_state.elem.classList.remove('resizer-dragging');

        document.removeEventListener('pointermove', move);
        document.removeEventListener('pointerup', stop);
        document.removeEventListener('pointercancel', stop);
    }

    const resizer = document.createElement('div');
    resizer.className = 'resizer';
    resizer.addEventListener('pointerdown', start);
    resizer.addEventListener('touchstart', (e) => e.preventDefault());

    header.appendChild(resizer);
}

function appendHeader(meta) {
    if (meta.length == 0) { return; }
    header = meta;

    let thead = document.createElement('thead');

    let th = document.createElement('th');
    th.className = 'row-number';
    th.appendChild(document.createTextNode('№'));
    thead.appendChild(th);

    /// Assign z-index in the reverse order.
    /// This is needed to make sure the contents that overflow to the right (column resizer) is visible.
    let zIndex = meta.length;

    meta.forEach(elem => {
        let th = document.createElement('th');
        const name = document.createTextNode(elem.name);
        const nameSpan = document.createElement('span');
        nameSpan.className = 'name';
        nameSpan.appendChild(name);
        th.appendChild(nameSpan);
        let type_hint = document.createElement('span');
        type_hint.className = 'type-hint';
        type_hint.appendChild(document.createTextNode(elem.type));
        th.appendChild(type_hint);
        appendHeaderResizer(th);
        th.style.zIndex = zIndex;
        --zIndex;
        thead.appendChild(th);

        column_is_number[elem.name] = !!elem.type.match(/^(Nullable\()?(U?Int|Decimal|Float)/);
    });

    let table = document.getElementById('data-table');
    while (table.firstChild) {
        table.removeChild(table.lastChild);
    }
    let tbody = document.createElement('tbody');
    tbody.id = 'tbody';
    table.appendChild(thead);
    table.appendChild(tbody);
    table.style.display = 'table';

    row_idx = 0;
}

const max_rows = 1_000;
const max_cells = 100_000;

let current_selected_cell = null;
function appendRow(row, fragment) {
    if (row_idx >= max_rows || (1 + row_idx) * Object.keys(header).length > max_cells) {
        incomplete_result = true;
        return false;
    }

    ++row_idx;
    let tr = document.createElement('tr');

    let td = document.createElement('td');
    td.className = 'row-number';
    td.appendChild(document.createTextNode(row_idx));
    tr.appendChild(td);

    let col_idx = 0;
    for (const column of header) {
        const td = renderCell(column.name, row[column.name]);
        ++col_idx;

        td.onclick = () => {
            current_selected_cell?.classList.remove('td-selected');
            current_selected_cell?.parentElement.classList.remove('tr-selected');
            current_selected_cell = td;
            current_selected_cell.classList.add('td-selected')
            current_selected_cell.parentElement.classList.add('tr-selected')
            current_selected_cell.scrollIntoView({ block: 'nearest' });
        };

        tr.appendChild(td);
    }

    fragment.appendChild(tr);
    incomplete_result = false;
    return true;
}

function renderExtremes() {
    let col_idx = 0;
    for (const column of header) {
        const name = column.name;
        ++col_idx;
        if (column_is_number[name]
            && extremes.min[name] !== undefined && extremes.max[name] !== undefined
            && Number(extremes.max[name]) > Number(extremes.min[name])) {
            let table = document.getElementById('data-table');
            for (let row of table.rows) {
                let cell = row.cells[col_idx];
                let value = +cell.innerText;
                if (value > 0) {
                    const ratio = 100 * value / extremes.max[name];

                    cell.style.cssText = `
                        background-size: 100% 50%;
                        background-position: center;
                        background-repeat: no-repeat;
                        background: linear-gradient(to right,
                            var(--bar-color) 0%, var(--bar-color) ${ratio}%,
                            var(--element-background-color) ${ratio}%, var(--element-background-color) 100%)`;
                }
            }
        }
    }
}

function renderError(message)
{
    document.getElementById('error').innerText = message ? message : "No response.";
    document.getElementById('error').style.display = 'block';
    document.getElementById('logo-container').style.display = 'none';
}

/// Huge JS libraries should be loaded only if needed.
function loadJS(src, integrity) {
    return new Promise((resolve, reject) => {
        const script = document.createElement('script');
        script.src = src;
        if (integrity) {
            script.crossOrigin = 'anonymous';
            script.integrity = integrity;
        } else {
            console.warn('no integrity for', src)
        }
        script.addEventListener('load', function() { resolve(true); });
        document.head.appendChild(script);
    });
}

let load_dagre_promise;
function loadDagre() {
    if (load_dagre_promise) { return load_dagre_promise; }

    load_dagre_promise = Promise.all([
        loadJS('https://dagrejs.github.io/project/dagre/v0.8.5/dagre.min.js',
                'sha384-2IH3T69EIKYC4c+RXZifZRvaH5SRUdacJW7j6HtE5rQbvLhKKdawxq6vpIzJ7j9M'),
        loadJS('https://dagrejs.github.io/project/graphlib-dot/v0.6.4/graphlib-dot.min.js',
                'sha384-Q7oatU+b+y0oTkSoiRH9wTLH6sROySROCILZso/AbMMm9uKeq++r8ujD4l4f+CWj'),
        loadJS('https://dagrejs.github.io/project/dagre-d3/v0.6.4/dagre-d3.min.js',
                'sha384-9N1ty7Yz7VKL3aJbOk+8ParYNW8G5W+MvxEfFL9G7CRYPmkHI9gJqyAfSI/8190W'),
        loadJS('https://cdn.jsdelivr.net/npm/d3@7.0.0',
                'sha384-S+Kf0r6YzKIhKA8d1k2/xtYv+j0xYUU3E7+5YLrcPVab6hBh/r1J6cq90OXhw80u'),
    ]);

    return load_dagre_promise;
}

async function renderGraph()
{
    clearElement('data-table');
    await loadDagre();

    /// https://github.com/dagrejs/dagre-d3/issues/131
    const dot = explain_graph.replace(/shape\s*=\s*box/g, 'shape=rect');

    let graph = graphlibDot.read(dot);
    let render = new dagreD3.render();

    graph.setGraph({
        nodesep: 20,
        rankdir: 'LR',
    });

    let svg = document.getElementById('graph');
    svg.style.display = 'block';

    render(d3.select("#graph"), graph);

    svg.style.width = graph.graph().width;
    svg.style.height = graph.graph().height;
}

let load_uplot_promise;
function loadUplot() {
    if (load_uplot_promise) { return load_uplot_promise; }
    load_uplot_promise = loadJS('https://cdn.jsdelivr.net/npm/uplot@1.6.21/dist/uPlot.iife.min.js',
        'sha384-TwdJPnTsKP6pnvFZZKda0WJCXpjcHCa7MYHmjrYDu6rsEsb/UnFdoL0phS5ODqTA');
    return load_uplot_promise;
}

function legendAsTooltipPlugin({ className, style = {} } = {}) {
    let legendEl;
    let multiline;

    function init(u, opts) {
        legendEl = u.root.querySelector(".u-legend");
        legendEl.classList.remove("u-inline");
        className && legendEl.classList.add(className);

        uPlot.assign(legendEl.style, {
            textAlign: "right",
            pointerEvents: "none",
            display: "none",
            position: "absolute",
            left: 0,
            top: 0,
            zIndex: 100,
            boxShadow: "2px 2px 10px rgba(0, 0, 0, 0.1)",
            ...style
        });

        const nodes = legendEl.querySelectorAll("th");
        for (let i = 0; i < nodes.length; i++)
            nodes[i]._order = i;

        if (opts.series.length == 2) {
            multiline = false;
            for (let i = 0; i < nodes.length; i++)
                nodes[i].style.display = "none";
        } else {
            multiline = true;
            legendEl.querySelector("th").remove();
            legendEl.querySelector("td").setAttribute('colspan', '2');
            legendEl.querySelector("td").style.textAlign = 'center';
            let footer = legendEl.insertRow().insertCell();
            footer.setAttribute('colspan', '2');
            footer.style.textAlign = 'center';
            footer.classList.add('u-value');
            footer.parentNode.classList.add('u-series','footer');
            footer.textContent = ". . .";
        }

        const overEl = u.over;
        overEl.style.overflow = "visible";

        overEl.appendChild(legendEl);

        overEl.addEventListener("mouseenter", () => {legendEl.style.display = null;});
        overEl.addEventListener("mouseleave", () => {legendEl.style.display = "none";});
    }

    function nodeListToArray(nodeList) {
        return Array.prototype.slice.call(nodeList);
    }

    function update(u) {
        let { left, top } = u.cursor;
        /// This will make the balloon to the right of the cursor when the cursor is on the left side, and vise-versa,
        /// avoiding the borders of the chart.
        left -= legendEl.clientWidth * (left / u.width);
        if (top >= legendEl.clientHeight) {
            top -= legendEl.clientHeight;
        }
        legendEl.style.transform = "translate(" + left + "px, " + top + "px)";

        if (multiline) {
            let nodes = nodeListToArray(legendEl.querySelectorAll("tr"));
            let header = nodes.shift();
            let footer = nodes.pop();
            let showLimit = Math.floor(u.height / 30);
            nodes.forEach(function (node) { node._sort_key = nodes.length > showLimit ? +node.querySelector("td").textContent.replace(/,/g,'') : node._order; });
            nodes.sort((a, b) => b._sort_key - a._sort_key);
            nodes.forEach(function (node) { node.parentNode.appendChild(node); });
            for (let i = 0; i < nodes.length; i++) {
                nodes[i].style.display = i < showLimit ? null : "none";
            }
            footer.parentNode.appendChild(footer);
            footer.style.display = nodes.length > showLimit ? null : "none";
        }
    }

    return {
        hooks: {
            init: init,
            setCursor: update,
        }
    };
}

let uplot;
async function renderChart(json)
{
    await loadUplot();
    clear();

    let chart = document.getElementById('chart');
    chart.style.display = 'block';

    let paths = json[0].length < chart.clientWidth / 5 ? uPlot.paths.bars() : uPlot.paths.linear();

    const [line_color, fill_color, grid_color, axes_color] = theme == 'light'
        ? ["rgba(255, 0, 200, 100%)", "rgba(255, 128, 200, 50%)", "#CCC", "#444"]
        : ["rgba(255, 255, 0, 100%)", "rgba(255, 255, 100, 50%)", "#444", "#CCC"];

    const opts = {
        width: chart.clientWidth,
        height: chart.clientHeight,
        scales: { x: { time: json[0][0] > 1000000000 && json[0][0] < 2000000000 } },
        axes: [ { stroke: axes_color,
                    grid: { width: 1 / devicePixelRatio, stroke: grid_color },
                    ticks: { width: 1 / devicePixelRatio, stroke: grid_color } },
                { stroke: axes_color,
                    grid: { width: 1 / devicePixelRatio, stroke: grid_color },
                    ticks: { width: 1 / devicePixelRatio, stroke: grid_color } } ],
        series: [ { label: "x" },
                    { label: "y", stroke: line_color, fill: fill_color, paths, points: { show: false } } ],
        padding: [ null, null, null, (Math.ceil(Math.log10(Math.max(...json[1]))) + Math.floor(Math.log10(Math.max(...json[1])) / 3)) * 6 ],
        plugins: [ legendAsTooltipPlugin() ],
    };

    uplot = new uPlot(opts, json, chart);
}

function resizeChart() {
    if (uplot) {
        let chart = document.getElementById('chart');
        uplot.setSize({ width: chart.clientWidth, height: chart.clientHeight });
    }
}

function redrawChart() {
    if (uplot && document.getElementById('chart').style.display == 'block') {
        renderChart(uplot.data);
    }
}

new ResizeObserver(resizeChart).observe(document.getElementById('chart'));

let lexer_module;
async function loadLexer() {
    // base64 -w0 build/src/Parsers/Lexer.wasm
    const lexer_base64 = "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A08NAkEBIQgDQCACLQAAIgVBMGshAQJAAkAgBwRAIAFB/wFxQQpJIAVBwQBrQQZJcg0CIAVB4QBrQQZPDQEMAgsgAUH/AXFBCkkNAQsgCCAFQd8AR3JBAXENBAJAIAJBAWoiCSADTw0AIAktAAAiBUEwayEBIAcEQCABQf8BcUEKSSAFQcEAa0EGSXINASAFQeEAa0EGTw0HDAELIAFB/wFxQQlLDQYLIAMgCUYNBAsgBiACQQFqIgI2AgBBACEIIAIgA0kNAAsMAgsgBiACQQFqIgI2AgBBACEIIAIgA0cNAAsLIAJBAWoiASADTw0AIAItAAAhBQJAIAcEQCAFQSByQfAARw0CDAELIAVBIHJB5QBHDQELIAYgATYCAAJAIAJBAmogA08NAAJAIAEtAABBK2sOAwABAAELIAYgAUEBaiIBNgIAIAEgA08NAgtBASECA0AgAS0AACIFQTBrQf8BcUEKTwRAIAIgBUHfAEdyQQFxDQMgAUEBaiICIANPDQMgAi0AAEEwa0H/AXFBCUsNAwsgBiABQQFqIgE2AgBBACECIAEgA0cNAAsMAQsgAiEBCwJ/AkACQCABIANPDQAgAS0AACICQd8ARiACQTBrQf8BcUEKSXJFIAJB3wFxQcEAa0H/AXFBGUtxDQAgAUEBaiEFA0AgBiAFIgE2AgAgASADTw0CIAFBAWohBSABLQAAIgdBMGshAiAHQd8ARiACQf8BcUEKSXIgB0HfAXFBwQBrQf8BcUEaSXINAAsgAUEBawwCCyAAIAE2AggMCgsgAUEBawshAwJAIAEgBE0NACAEQQFrIQUDQCAFQQFqIgUtAAAiAkEkRiACQd8ARnIgAkEwa0H/AXFBCklyRSACQd8BcUHBAGtB/wFxQRpPcUUEQCADIAVLDQEMAgsLIAAgATYCCCAAIAQ2AgQgAEEvOgAADwsMCQsgBiAEQQJqIgE2AgACQCABIANGDQADQCABLQAAQQpGDQEgAUEBaiIBIANHDQALIAMhAQsLIAAgATYCCCAAIAQ2AgQgAEEBOgAACyAGIAE2AgAPCyAAIAE2AgggACAENgIEIABBADoAAA8LIAAgBEEBaiIANgIIDAILIAAgBEECaiIANgIIDAELIAAgATYCCCAAIAQ2AgQgAEEoOgAADwsgBiAANgIADwsgACAENgIEIABBAzoAAA8LIAAgATYCCCAAIAQ2AgQgAEECOgAACyUAIABBADoAECAAIAM2AgwgACACNgIIIAAgATYCBCAAIAE2AgALNgEBfyMAQRBrIgMkACADQQRqIAAQASABIAMoAgg2AgAgAiADKAIMNgIAIAMtAAQgA0EQaiQACwcAIABBAUsLBwAgAEEnSwsHACAAQSdGCwsIAQBBgAgLARQAeQlwcm9kdWNlcnMBDHByb2Nlc3NlZC1ieQEFY2xhbmdZMjAuMC4wZ2l0IChnaXRAZ2l0aHViLmNvbTpsbHZtL2xsdm0tcHJvamVjdC5naXQgNmNiYzY0ZWQ5MjJjYzY5YmMyOTJkMzk0YmE1YzY4MWZhMzA5ZjQwNCkAaw90YXJnZXRfZmVhdHVyZXMGKw9tdXRhYmxlLWdsb2JhbHMrE25vbnRyYXBwaW5nLWZwdG9pbnQrC2J1bGstbWVtb3J5KwhzaWduLWV4dCsPcmVmZXJlbmNlLXR5cGVzKwptdWx0aXZhbHVl";

    if (!lexer_module) {
        const binary = atob(lexer_base64);
        const bytes = new Uint8Array(binary.length);

        for (let i = 0; i < binary.length; i++) {
            bytes[i] = binary.charCodeAt(i);
        }

        lexer_module = await WebAssembly.instantiate(bytes);
    }
}

async function tokenize(query) {
    await loadLexer();

    let exports = lexer_module.instance.exports;
    let buffer = exports.memory.buffer;
    let memory_offset = 0;

    /// Allocate memory for the lexer object
    const lexer = new Uint8Array(buffer, memory_offset, exports.clickhouse_lexer_size);
    memory_offset += exports.clickhouse_lexer_size;

    /// Allocate the query
    const bytes = new TextEncoder().encode(query);
    const query_array = new Uint8Array(buffer, memory_offset, bytes.length);
    query_array.set(bytes);
    const query_begin = memory_offset;
    memory_offset += bytes.length;
    const query_end = memory_offset;

    /// Initialize the lexer
    exports.clickhouse_lexer_create(lexer, query_begin, query_end, 65536);

    /// Allocate the out ptrs
    const token_begin = memory_offset;
    memory_offset += 4;
    const token_end = memory_offset;
    memory_offset += 4;

    let result = [];

    while (true) {
        const token_type = exports.clickhouse_lexer_next_token(lexer, token_begin, token_end);
        if (exports.clickhouse_lexer_token_is_error(token_type) || exports.clickhouse_lexer_token_is_end(token_type)) {
            break;
        }

        const view = new DataView(buffer);
        const begin = view.getUint32(token_begin, true);
        const end = view.getUint32(token_end, true);

        const token_bytes = new Uint8Array(buffer, begin, end - begin);
        let token = new TextDecoder().decode(token_bytes);

        result.push({type: token_type, significant: exports.clickhouse_lexer_token_is_significant(token_type), token: token});
    }

    return result;
}

async function getQueryUnderCursor() {
    const all_queries = query_area.value;

    if (typeof WebAssembly !== 'object' || typeof WebAssembly.instantiate !== 'function') {
        return all_queries;
    }

    const tokens = await tokenize(all_queries);

    const cursor_position = query_area.selectionStart;
    let current_query_start = 0;
    let current_offset = 0;

    for (const elem of tokens) {
        if (current_query_start == current_offset && !elem.significant) {
            current_query_start += elem.token.length;
        }
        current_offset += elem.token.length;
        if (elem.token == ';') {
            if (current_offset >= cursor_position) {
                query_area.setSelectionRange(current_query_start, current_offset);
                query_area.focus();
                return all_queries.substring(current_query_start, current_offset);
            } else {
                current_query_start = current_offset;
            }
        }

    }

    return all_queries;
}

/// First we check if theme is set via the 'theme' GET parameter, if not, we check localStorage, otherwise we check OS preference.
let theme = current_url.searchParams.get('theme');
if (['dark', 'light'].indexOf(theme) === -1) {
    theme = window.localStorage.getItem('theme');
}
if (!theme) {
    theme = 'light';
}

function setColorTheme(new_theme, update_preference) {
    theme = new_theme;
    if (update_preference) {
        window.localStorage.setItem('theme', theme);
    }
    document.documentElement.setAttribute('data-theme', theme);
    redrawChart();
}

if (theme) {
    document.documentElement.setAttribute('data-theme', theme);
} else {
    /// Obtain system-level user preference
    const media_query_list = window.matchMedia('(prefers-color-scheme: dark)');
    if (media_query_list.matches) {
        setColorTheme('dark');
    }

    /// There is a rumor that on some computers, the theme is changing automatically on day/night.
    media_query_list.addEventListener('change', function(e) {
        setColorTheme(e.matches ? 'dark' : 'light');
    });
}

if (run_immediately) {
    post();
}

document.getElementById('toggle-light').onclick = function() {
    setColorTheme('light', true);
}

document.getElementById('toggle-dark').onclick = function() {
    setColorTheme('dark', true);
}
</script>
</html>
